﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="LoopItemsPanel.cs" company="">
//   
// </copyright>
// <summary>
//   The loop items panel.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace CustomDatePicker.Controls.DatePicker
{
    using System;
    using System.Linq;

    using CustomDatePicker.Controls.DatePicker.Common;

    using Windows.Foundation;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Controls.Primitives;
    using Windows.UI.Xaml.Input;
    using Windows.UI.Xaml.Media;
    using Windows.UI.Xaml.Media.Animation;

    /// <summary>
    ///     The loop items panel.
    /// </summary>
    public class LoopItemsPanel : Panel
    {
        #region Public Properties

        /// <summary>
        ///     The selected item changed.
        /// </summary>
        public event EventHandler<PickerSelectorItem> SelectedItemChanged;

        /// <summary>
        ///     Gets or sets the scroll action.
        /// </summary>
        public ScrollAction ScrollAction
        {
            get;
            set;
        }

        /// <summary>
        ///     Gets the parent date picker.
        /// </summary>
        public DatePicker ParentDatePicker
        {
            get
            {
                return this.parentDatePicker ?? (this.parentDatePicker = this.GetVisualAncestor<DatePicker>());
            }
        }

        /// <summary>
        ///     Gets the parent date picker selector.
        /// </summary>
        public PickerSelector ParentDatePickerSelector
        {
            get
            {
                return this.parentDatePickerSelector
                       ?? (this.parentDatePickerSelector = this.GetVisualAncestor<PickerSelector>());
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        ///     The slider vertical. Hack to animate a value easely
        /// </summary>
        private readonly Slider sliderVertical;

        /// <summary>
        ///     The item height. must be 1d to fire first arrangeoverride
        /// </summary>
        private double itemHeight = 1d;

        /// <summary>
        ///     The animation duration.
        /// </summary>
        private readonly TimeSpan animationDuration = TimeSpan.FromMilliseconds(400);

        /// <summary>
        ///     The template applied.
        ///     True when arrange override is ok
        /// </summary>
        private bool templateApplied;

        /// <summary>
        ///     The parent date picker.
        /// </summary>
        private DatePicker parentDatePicker;

        /// <summary>
        ///     The parent date picker selector.
        /// </summary>
        private PickerSelector parentDatePickerSelector;

        /// <summary>
        ///     The storyboard.
        /// </summary>
        private Storyboard storyboard;

        /// <summary>
        ///     The animation snap.
        /// </summary>
        private DoubleAnimation animationSnap;

        /// <summary>
        /// Gets or sets a value indicating whether is first layout passed.
        /// </summary>
        public bool IsFirstLayoutPassed
        {
            get;
            set;
        }

        #endregion

        #region Internal Properties

        /// <summary>
        ///     The short animation duration.
        /// </summary>
        internal readonly TimeSpan ShortAnimationDuration = TimeSpan.FromMilliseconds(100);

        #endregion

        #region Constructor

        /// <summary>
        ///     Initializes a new instance of the <see cref="LoopItemsPanel" /> class.
        ///     Ctor
        /// </summary>
        public LoopItemsPanel()
        {
            this.ManipulationMode = ManipulationModes.TranslateY | ManipulationModes.TranslateInertia;
            this.IsHitTestVisible = true;
            this.ManipulationDelta += this.OnManipulationDelta;
            this.ManipulationCompleted += this.OnManipulationCompleted;
            this.Tapped += this.OnTapped;
            this.Loaded += this.OnLoaded;

            this.sliderVertical = new Slider
            {
                SmallChange = 0.0000000001,
                Minimum = double.MinValue,
                Maximum = double.MaxValue,
                StepFrequency = 0.0000000001
            };
            this.sliderVertical.ValueChanged += this.OnVerticalOffsetChanged;

            this.CreateStoryboard();

            this.LayoutUpdated += this.OnLayoutUpdated;
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Scroll to a selected index (with animation or not)
        /// </summary>
        /// <param name="selectedItem">
        /// The selected Item.
        /// </param>
        /// <param name="duration">
        /// The duration.
        /// </param>
        internal void ScrollToSelectedIndex(PickerSelectorItem selectedItem, TimeSpan duration)
        {
            if (!this.templateApplied)
            {
                return;
            }

            var centerTopOffset = (this.ActualHeight / 2d) - this.itemHeight / 2d;

            var deltaOffset = centerTopOffset - selectedItem.GetVerticalPosition();

            if (double.IsInfinity(deltaOffset) || double.IsNaN(deltaOffset))
            {
                return;
            }

            if (duration == TimeSpan.Zero)
            {
                this.sliderVertical.Value += deltaOffset;
            }
            else
            {
                this.UpdatePositionsWithAnimationAsync(selectedItem, deltaOffset, duration);
            }
        }

        /// <summary>
        ///     Get the centered item
        /// </summary>
        /// <returns>
        ///     The <see cref="PickerSelectorItem" />.
        /// </returns>
        internal PickerSelectorItem GetMiddleItem()
        {
            // Récupère la taille du panel
            var actualHeight = this.ActualHeight;
            var actualWidth = this.ActualWidth;

            // Récupère la position de la moitié de mon viewportheight
            var middleY = actualHeight / 2;
            var middleX = actualWidth / 2;

            // Génération d'un point
            var middlePosition = new Point(middleX, middleY);

            // ReSharper disable once PossibleInvalidCastExceptionInForeachLoop
            return (from PickerSelectorItem uiElement in this.Children where uiElement != null let rect = uiElement.TransformToVisual(this).TransformBounds(new Rect()) where middlePosition.Y <= rect.Y + uiElement.RenderSize.Height && middlePosition.Y >= rect.Y select uiElement).FirstOrDefault();
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Arrange all items
        /// </summary>
        /// <param name="finalSize">
        /// The final Size.
        /// </param>
        /// <returns>
        /// The <see cref="Size"/>.
        /// </returns>
        protected override Size ArrangeOverride(Size finalSize)
        {
            // Clip to ensure items dont override container
            this.Clip = new RectangleGeometry
            {
                Rect = new Rect(0, 0, finalSize.Width, finalSize.Height)
            };

            if (this.Children == null || this.Children.Count == 0)
            {
                var size = base.ArrangeOverride(finalSize);
                var width = double.IsInfinity(size.Width) ? 400 : size.Width;
                var height = double.IsInfinity(size.Height) ? 800 : size.Height;

                return new Size(width, height);
            }

            var positionTop = 0d;
            var positionLeft = 0d;
            PickerSelectorItem selectedItem = null;

            // Must Create looping items count
            // ReSharper disable once PossibleInvalidCastExceptionInForeachLoop
            foreach (PickerSelectorItem item in this.Children)
            {
                if (item == null)
                {
                    continue;
                }

                var desiredSize = item.DesiredSize;

                if (double.IsNaN(desiredSize.Width) || double.IsNaN(desiredSize.Height))
                {
                    continue;
                }

                item.RectPosition = new Rect(positionLeft, positionTop, desiredSize.Width, desiredSize.Height);

                item.Arrange(item.RectPosition);

                if (item.IsSelected)
                {
                    selectedItem = item;
                }

                positionTop += desiredSize.Height;
            }

            this.templateApplied = true;

            if (selectedItem != null)
            {
                this.UpdatePositions(this.sliderVertical.Value);
                this.ScrollToSelectedIndex(selectedItem, TimeSpan.Zero);
            }

            return finalSize;
        }

        /// <summary>
        /// Measure items
        /// </summary>
        /// <param name="availableSize">
        /// The available Size.
        /// </param>
        /// <returns>
        /// The <see cref="Size"/>.
        /// </returns>
        protected override Size MeasureOverride(Size availableSize)
        {
            var width = double.IsInfinity(availableSize.Width) ? 400 : availableSize.Width;
            var height = double.IsInfinity(availableSize.Height) ? 800 : availableSize.Height;

            var nSize = new Size(width, height);

            // Measure all items
            foreach (var container in this.Children)
            {
                // container.Measure(availableSize);
                container.Measure(nSize);

                if (this.itemHeight != container.DesiredSize.Height)
                {
                    this.itemHeight = container.DesiredSize.Height;
                }
            }

            this.itemHeight = 100;

            return nSize;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// The on loaded.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="routedEventArgs">
        /// The routed event args.
        /// </param>
        private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
            this.InvalidateArrange();
        }

        /// <summary>
        /// The on layout updated.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="o">
        /// The o.
        /// </param>
        private void OnLayoutUpdated(object sender, object o)
        {
            this.IsFirstLayoutPassed = true;

            this.LayoutUpdated -= this.OnLayoutUpdated;
        }

        /// <summary>
        /// Click or Tap on an item
        ///     On Items Panel, the tap action only scroll to selected tap object
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="args">
        /// The args.
        /// </param>
        private void OnTapped(object sender, TappedRoutedEventArgs args)
        {
            if (this.Children == null || this.Children.Count == 0)
            {
                return;
            }

            // if not focused, dont do anything
            if (this.ParentDatePickerSelector != null && this.ParentDatePicker != null)
            {
                if (!this.ParentDatePickerSelector.IsFocused && this.ParentDatePicker.EnableFirstTapHasFocusOnly)
                {
                    return;
                }
            }

            var positionY = args.GetPosition(this).Y;

            foreach (var uiElement in this.Children)
            {
                var child = (PickerSelectorItem)uiElement;

                var childPositionY = child.GetVerticalPosition();
                var height = child.RectPosition.Height;

                if (!(positionY >= childPositionY) || !(positionY <= (childPositionY + height)))
                {
                    continue;
                }

                this.ScrollToSelectedIndex(child, this.animationDuration);

                break;
            }
        }

        /// <summary>
        /// When manipulation is completed, go to the closest item
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void OnManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
        {
            var middleItem = this.GetMiddleItem();

            // get rect
            if (middleItem == null)
            {
                return;
            }

            // animate and scroll to
            this.ScrollToSelectedIndex(middleItem, this.animationDuration);
        }

        /// <summary>
        /// On manipulation delta
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void OnManipulationDelta(object sender, ManipulationDeltaRoutedEventArgs e)
        {
            this.ScrollAction = e.Delta.Translation.Y > 0 ? ScrollAction.Down : ScrollAction.Up;

            // get translation
            var translation = e.Delta.Translation;

            // update position
            this.sliderVertical.Value += translation.Y / 2;
        }

        /// <summary>
        /// During an animation of the Scrollbar, update positions
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The e.
        /// </param>
        private void OnVerticalOffsetChanged(object sender, RangeBaseValueChangedEventArgs e)
        {
            this.UpdatePositions(e.NewValue);
        }

        /// <summary>
        ///     Get Items Source count items
        /// </summary>
        /// <returns>The number of children.</returns>
        private int GetItemsCount()
        {
            return this.Children != null ? this.Children.Count : 0;
        }

        /// <summary>
        ///     Creating the storyboard
        /// </summary>
        private void CreateStoryboard()
        {
            this.storyboard = new Storyboard();
            this.animationSnap = new DoubleAnimation
            {
                EnableDependentAnimation = true
            };

            this.storyboard.Children.Add(this.animationSnap);

            Storyboard.SetTarget(this.animationSnap, this.sliderVertical);
            Storyboard.SetTargetProperty(this.animationSnap, "Value");
        }

        /// <summary>
        /// Updating with an animation (after a tap)
        /// </summary>
        /// <param name="selectedItem">
        /// The selected Item.
        /// </param>
        /// <param name="delta">
        /// The delta.
        /// </param>
        /// <param name="duration">
        /// The duration.
        /// </param>
        private void UpdatePositionsWithAnimationAsync(PickerSelectorItem selectedItem, double delta, TimeSpan duration)
        {
            this.animationSnap.From = selectedItem.GetTranslateTransform().Y;
            this.animationSnap.To = selectedItem.GetTranslateTransform().Y + delta;
            this.animationSnap.Duration = duration;
            this.animationSnap.EasingFunction = new ExponentialEase
            {
                EasingMode = EasingMode.EaseInOut
            };

            this.sliderVertical.ValueChanged -= this.OnVerticalOffsetChanged;
            this.sliderVertical.Value = selectedItem.GetTranslateTransform().Y;
            this.sliderVertical.ValueChanged += this.OnVerticalOffsetChanged;

            this.storyboard.Completed += (sender, o) =>
            {
                if (this.SelectedItemChanged != null)
                {
                    this.SelectedItemChanged(this, selectedItem);
                }
            };

            this.storyboard.RunAsync();
        }

        /// <summary>
        /// Updating position
        /// </summary>
        /// <param name="offsetDelta">
        /// The offset Delta.
        /// </param>
        private void UpdatePositions(double offsetDelta)
        {
            var maxLogicalHeight = this.GetItemsCount() * this.itemHeight;

            var offset = offsetDelta % maxLogicalHeight;

            // Get the correct number item
            var itemNumberSeparator = (Int32)(Math.Abs(offset) / this.itemHeight);

            int itemIndexChanging;
            double offsetAfter;
            double offsetBefore;

            if (offset > 0)
            {
                itemIndexChanging = this.GetItemsCount() - itemNumberSeparator - 1;
                offsetAfter = offset;

                if (offset % maxLogicalHeight == 0)
                {
                    itemIndexChanging++;
                }

                offsetBefore = offsetAfter - maxLogicalHeight;
            }
            else
            {
                itemIndexChanging = itemNumberSeparator;
                offsetBefore = offset;
                offsetAfter = maxLogicalHeight + offsetBefore;
            }

            // items that must be before
            this.UpdatePosition(itemIndexChanging, this.GetItemsCount(), offsetBefore);

            // items that must be after
            this.UpdatePosition(0, itemIndexChanging, offsetAfter);
        }

        /// <summary>
        /// Translate items to a new offset
        /// </summary>
        /// <param name="startIndex">
        /// The start Index.
        /// </param>
        /// <param name="endIndex">
        /// The end Index.
        /// </param>
        /// <param name="offset">
        /// The offset.
        /// </param>
        private void UpdatePosition(int startIndex, int endIndex, double offset)
        {
            for (var i = startIndex; i < endIndex; i++)
            {
                var loopListItem = this.Children[i] as PickerSelectorItem;
                if (loopListItem == null)
                {
                    continue;
                }

                var translateTransform = loopListItem.GetTranslateTransform();

                translateTransform.Y = offset;
            }
        }

        #endregion
    }
}
